/*
Copyright (c) 2008 Tor Lye

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package com.googlecode.pidginsasm;

import java.awt.Component;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.imageio.ImageIO;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JProgressBar;
import javax.swing.JTextField;
import javax.swing.SwingWorker;
import javax.swing.SwingWorker.StateValue;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;


public class SometingAwfulSmileyDownloader {
	
	private final static String version = "0.9.1";
	
	private final static String URL = "http://forums.somethingawful.com/" +
			"misc.php?action=showsmilies";
	private JTextField fileField;
	private JFileChooser fileChooser;
	private ArrayList<JCheckBox> checkBoxes = new ArrayList<JCheckBox>();
	private JButton fileChooserButton;
	JButton goButton;
	private Map<String, String> sections;
	private JProgressBar progressBar;
	private Container contentPane;
	private Stack<DownloadTask> taskStack = new Stack<DownloadTask>();
	
	private final static byte[] icon = new byte[] {
		0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x10, 0x00, 0x10, 0x00, -128, 0x01,
		0x00, 0x00, 0x00, 0x00, -1, -1, -1, 0x21, -7, 0x04, 0x01, 0x0a, 0x00, 
		0x01, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00,
		0x02, 0x26, -116, -127, 0x09, -58, 0x1d, -86, 0x5c, -125, 0x49, -98, 
		-6, -106, -92, 0x0b, 0x39, -82, 0x79, 0x17, -104, 0x31, -96, 0x27, 
		0x66, -28, 0x63, -98, -101, -5, -63, 0x31, 0x65, -87, 0x50, 0x6d, -89,
		0x75, 0x24, 0x15, 0x00, 0x3b
	};

	public static void main(String[] args) {
		new SometingAwfulSmileyDownloader();
	}


	public SometingAwfulSmileyDownloader() {
		try {
			sections = getSections(readHtml(URL));
			
			String path;
			if(System.getProperty("os.name").toLowerCase().contains("windows"))
				path = System.getenv("APPDATA");
			else
				path = System.getProperty("user.home");
			if(!path.endsWith(File.separator))
				path += File.separator;
			path += ".purple";
			
			fileField = new JTextField(path);
			createGUI();
			
		} catch (Exception e) {
			Component parentComponent = null;
			if (contentPane != null && contentPane.isShowing())
				parentComponent = contentPane;
			
			JOptionPane.showMessageDialog(parentComponent, 
					"Now look what you've done! You completely crashed the\n" +
					"application. This is why we can't have nice things.", 
					"Something went horribly wrong!", 
					JOptionPane.ERROR_MESSAGE);
			
			System.err.println("CRASH!");
			e.printStackTrace();
		}
	}
	
	private void createGUI() {
		JFrame frame = new JFrame("Something Awful Smiley Downloader " + 
				version);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		try {
			ByteArrayInputStream stream = new ByteArrayInputStream(icon);
			frame.setIconImage(ImageIO.read(stream));
		} catch (IOException e) {}
		
		
		contentPane = frame.getContentPane();
		contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS));

		JLabel label = new JLabel("Include sections:");
		contentPane.add(label);
		
		for (String sectionName : sections.keySet()) {
			JCheckBox box = new JCheckBox(sectionName);
			contentPane.add(box);
			checkBoxes.add(box);
		}
		
		contentPane.add(new JLabel(" "));
		
		contentPane.add(new JLabel("Pidgin user settings path:"));
		Container fileChooserContainer = new Container();
		fileChooserContainer.setLayout(new BoxLayout(fileChooserContainer, 
				BoxLayout.X_AXIS));
		fileField.setEditable(false);
		fileChooserButton = new JButton("Choose");
		fileChooserContainer.add(fileField);
		fileChooserContainer.add(fileChooserButton);
		contentPane.add(fileChooserContainer);
		fileChooser = new JFileChooser();
		fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
		fileChooser.setSelectedFile(new File(fileField.getText()));
		
		contentPane.add(new JLabel(" "));
		Container progressContainer = new Container();
		progressContainer.setLayout(new BoxLayout(progressContainer, 
				BoxLayout.X_AXIS));
		goButton = new JButton("Go!");
		progressBar = new JProgressBar(0, 100);
		progressBar.setValue(0);
		progressBar.setStringPainted(true);
		progressContainer.add(goButton);
		progressContainer.add(progressBar);
		contentPane.add(progressContainer);
		
		fileChooserButton.addActionListener(new ActionListener(){
			public void actionPerformed(ActionEvent e) {
				int returnVal = fileChooser.showOpenDialog(contentPane);
				
				if (returnVal == JFileChooser.APPROVE_OPTION) {
					try {
						fileField.setText(fileChooser
								.getSelectedFile().getCanonicalPath());
					} catch (IOException ioe) {
						fileField.setText("");
					}
				}
			}
		});
		
		goButton.addActionListener(new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent e) {
				if(verifyPath(fileChooser.getSelectedFile())) {
					goButton.setEnabled(false);
					fileChooserButton.setEnabled(false);
					for (JCheckBox c : checkBoxes)
						c.setEnabled(false);
					
					for(JCheckBox box : checkBoxes) {
						if(box.isSelected()) {
							progressBar.setValue(0);
							
							DownloadTask task = new DownloadTask(
									fileChooser.getSelectedFile(), 
									sections.get(box.getText()));
							task.addPropertyChangeListener(
									new TaskPropertyChangeListener());
							taskStack.push(task);
						}
					}
					try {
						taskStack.pop().execute();
					} catch (EmptyStackException empty) { 
						doneMessage(); 
					}
				} else {
					JOptionPane.showMessageDialog(contentPane, 
							"Invalid pidgin directory or insufficient " +
							"access rights.\nCheck that you've " +
							"got Pidgin version 2.5.0 or newer,\nand that " +
							"you selected the correct directory.", 
							"Error", JOptionPane.ERROR_MESSAGE);
				}
			}
		});

		frame.pack();
		frame.setVisible(true);
	}
	
	private class TaskPropertyChangeListener implements 
			PropertyChangeListener {
		@Override
		public void propertyChange(PropertyChangeEvent evt) {
			if ("progress" == evt.getPropertyName() )
				progressBar.setValue((Integer)evt.getNewValue());
			
			else if("state" == evt.getPropertyName()) {
				if(((StateValue)evt.getNewValue()).equals(StateValue.DONE)) {
					try {
						taskStack.pop().execute();
					} catch (EmptyStackException empty) { 
						doneMessage(); 
					}
				}
			}
		}
	}
	
	private void doneMessage() {
		JOptionPane.showMessageDialog(contentPane, 
				"All done! ^^", "", 
				JOptionPane.INFORMATION_MESSAGE);
		
		goButton.setEnabled(true);
		fileChooserButton.setEnabled(true);
		for (JCheckBox c : checkBoxes)
			c.setEnabled(true);
	}

	private String readHtml(String urlString) throws IOException {
		URL url = new URL(urlString);
		
		String html = "";
		BufferedReader in = new BufferedReader(new InputStreamReader(
				url.openStream()));
		String inputLine;
		while ((inputLine = in.readLine()) != null)
			html = html+" "+inputLine;
		in.close();
		return html;
	}
	
	private Map<String, String> getSections(String html) 
	throws TonightWeDineInHellException {
		Matcher sectionMatcher = Pattern.compile("<h3>(.+?)</h3>\\s+" +
				"<ul class=\"smilie_group\">(.+?)</ul>").matcher(html);
		
		LinkedHashMap<String, String> sections = 
			new LinkedHashMap<String, String>();
		
		while (sectionMatcher.find())
			sections.put(
					sectionMatcher.group(1).replace("&amp;", "&"), 
					sectionMatcher.group(2));
		
		if (sections.size() < 1)
			throw new TonightWeDineInHellException("No sections found!");
		
		return sections;
	}
	
	private boolean verifyPath(File pidginPath) {
		
		String path;
		try {
			path = pidginPath.getCanonicalPath();
		} catch (IOException e) {
			return false;
		}
		
		if(!pidginPath.exists() || !pidginPath.isDirectory() || 
				!pidginPath.canRead()) {
			return false;
		}
		
		if(!path.endsWith(File.separator))
			path += File.separator;
		
		File xmlFile0 = new File(path + "accounts.xml");
		File xmlFile1 = new File(path + "prefs.xml");
		
		if(!xmlFile0.exists() && !xmlFile1.exists()) {
			return false;
		}
		
		File xmlFile = new File(path + "smileys.xml");
		if(xmlFile.exists()) {
			if(xmlFile.isDirectory() || !xmlFile.canRead() || 
					!xmlFile.canWrite())
				return false;
		} else {
			try {
				xmlFile.createNewFile();
				PrintWriter out = new PrintWriter(xmlFile);
				out.println("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
				out.println("");
				out.println("<smileys version=\"1.0\">");
				out.println("\t<profile name=\"Default\">");
				out.println("\t\t<smiley_set>");
				out.println("\t\t</smiley_set>");
				out.println("\t</profile>");
				out.println("</smileys>");
				out.close();
			} catch(IOException ioe) {
				return false;
			}
		}
		
		File smileyPath = new File(path + "custom_smiley");
		if(smileyPath.exists()) {
			if(!smileyPath.isDirectory() || !smileyPath.canRead() || 
					!smileyPath.canWrite())
				return false;
		} else {
			smileyPath.mkdir();
		}
		return true;
	}

}

class DownloadTask extends SwingWorker<Void, Void> {
	
	private File pidginDir;
	private String html;
	
	public DownloadTask(File pidginDir, String html) {
		this.pidginDir = pidginDir;
		this.html = html;
	}

	@Override
	protected Void doInBackground() throws Exception {
		setProgress(0);
		
		ArrayList<String> smilies = new ArrayList<String>();
		ArrayList<String> urls = new ArrayList<String>();
		
		Matcher matcher = Pattern.compile(
				"<div class=\"text\">(.+?)</div>\\s+" +
				"<img.+?src=\"(.+?)\"").matcher(html);
		
		while (matcher.find()) {
			smilies.add(matcher.group(1));
			urls.add(matcher.group(2));
		}
		
		String pidginPath = pidginDir.getCanonicalPath();
		
		if(!pidginPath.endsWith(File.separator))
			pidginPath += File.separator;
		
		File imageFolder = new File(pidginPath + "custom_smiley");
		Pattern filenamePattern = 
			Pattern.compile("/([^/]+.((gif)|(png)|(jpg)|(jpeg)))");
		
		File xmlFile = new File(pidginPath + "smileys.xml");
		
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder = factory.newDocumentBuilder();
		Document xmldoc = builder.parse(xmlFile);
		
		Element parent = 
			(Element) xmldoc.getElementsByTagName("smiley_set").item(0);
		
		Map<String, String> smileycopy = new HashMap<String, String>();
		smileycopy.put(":3:", ":3");
		smileycopy.put(":eek:", ":o");
		smileycopy.put(":aaaaa:", ":O");
		smileycopy.put(":D", ":-D");
		smileycopy.put(":(", ":-(");
		smileycopy.put(":)", ":-)");
		smileycopy.put(":keke:", "^^");
		smileycopy.put(":keke: ", "^_^");
		smileycopy.put(":xd:", "XD");
		smileycopy.put(":xd: ", "xD");
		smileycopy.put(":swoon:", ":d");
		smileycopy.put(":v:", ":V");
		smileycopy.put(":v: ", ":v");
		smileycopy.put(":confused:", ":?");
		smileycopy.put(":downs:", ":=)");
		smileycopy.put(":saddowns:", ":=(");
		smileycopy.put(":=(", ">:(");
		smileycopy.put(";)", ";-)");
		smileycopy.put(":j:", ":girlv:");
		
		for (String key : smileycopy.keySet()) {
			int index = smilies.indexOf(key.trim());
			if(index != -1) {
				smilies.add(smileycopy.get(key));
				urls.add(urls.get(index));
			}
		}

		mainloop:
		for (int i=0; i<smilies.size(); i++) {
			String smileyKey = smilies.get(i);
			String urlString = urls.get(i);
			
			Matcher filenameMatcher = filenamePattern.matcher(urlString);
			if(filenameMatcher.find()) {
				String originalFilePath = imageFolder.getCanonicalPath() + 
						File.separator + filenameMatcher.group(1);
				
				setProgress((int)Math.floor(((double)100/smilies.size()) * i));
				
				NodeList children = parent.getChildNodes();
				
				for (int j = 0; j < children.getLength(); j++) {
					NamedNodeMap attributes = children.item(j).getAttributes();
					if(attributes != null && 
							attributes.getNamedItem("shortcut") != null) {
						if(attributes.getNamedItem("shortcut").getNodeValue()
								.equals(smileyKey)) {
//							System.err.println("Skipped "+smileyKey);
							continue mainloop;
						}
					}
				}
//				System.out.println("Adding "+smileyKey);

				URL url = new URL(urlString);

				MessageDigest md = MessageDigest.getInstance("SHA-1");

				DigestInputStream shaStream = 
					new DigestInputStream(url.openStream(), md);
				BufferedOutputStream out = 
					new BufferedOutputStream(
							new FileOutputStream(originalFilePath));

				byte[] buffer = new byte[1024];

				int numRead;
				while ((numRead = shaStream.read(buffer)) != -1) {
					out.write(buffer, 0, numRead);
				}
				
				shaStream.close();
				out.close();

				byte[] checksum = md.digest();
				String checkSumString = "";

				for(byte b : checksum) {
					String hexString = Integer.toHexString(b);
					if(hexString.length() == 1)
						hexString = "0" + hexString;
					else if(hexString.length() == 8)
						hexString = hexString.substring(6);
					checkSumString += hexString;
				}
				
				File imageFile = new File(originalFilePath);
				String newFileName = checkSumString + "." + 
						filenameMatcher.group(2);
				File newFile = new File(imageFolder.getCanonicalPath() + 
						File.separator + newFileName);
				if(newFile.exists())
					imageFile.delete();
				else
					imageFile.renameTo(newFile);
				
				Element newChild = xmldoc.createElement("smiley");
				newChild.setAttribute("shortcut", smileyKey);
				newChild.setAttribute("checksum", checkSumString);
				newChild.setAttribute("filename", newFileName);
				
				parent.appendChild(newChild);

			}
		}
		
		Transformer transformer = 
			TransformerFactory.newInstance().newTransformer();
		transformer.setOutputProperty(OutputKeys.ENCODING, "UTF8");
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		transformer.transform(
				new DOMSource(xmldoc), 
				new StreamResult(xmlFile));
		
		setProgress(100);
		return null;
	}
}

class TonightWeDineInHellException extends Exception {
	private static final long serialVersionUID = -5151376936058536808L;

	public TonightWeDineInHellException(String string) {
		super(string);
	}
}
